<h1>在正确的位置调用内置<code>recover</code>函数</h1>

<p>
恐慌/恢复机制已经<a href="control-flows-more.html#panic-recover">在前面的文章中介绍了</a>，
另外<a href="panic-and-recover-use-cases.html">上一篇文章</a>也介绍了一些恐慌/恢复用例。
通过这些介绍，我们得知一个<code>recover</code>调用只能在一个延迟函数调用中起作用。
但是，并非所有的处于延迟函数调用中的<code>recover</code>调用都能起作用。
本文下面将展示一些不起作用的<code>recover</code>调用，并解释它们为什么不起作用。
</p>

<div>
我们先看一个例子：
<pre class="line-numbers"><code class="language-go">package main

import "fmt"

func main() {
	defer func() {
		defer func() {
			fmt.Println("7:", recover())
		}()
	}()
	defer func() {
		func() {
			fmt.Println("6:", recover())
		}()
	}()
	func() {
		defer func() {
			fmt.Println("1:", recover())
		}()
	}()
	func() {
		defer fmt.Println("2:", recover())
	}()
	func() {
		fmt.Println("3:", recover())
	}()
	fmt.Println("4:", recover())
	defer fmt.Println("5:", recover())
	panic(789)
	defer func() {
		fmt.Println("0:", recover())
	}()
}
</code></pre>

运行之，我们将发现上例中的7个<code>recover</code>函数调用都没有恢复程序中产生的恐慌。
此程序的输出结果如下：
<pre class="output"><code>1: &lt;nil&gt;
2: &lt;nil&gt;
3: &lt;nil&gt;
4: &lt;nil&gt;
5: &lt;nil&gt;
6: &lt;nil&gt;
7: &lt;nil&gt;
panic: 789

goroutine 1 [running]:
...
</code></pre>

显然地，标号为0的<code>recover</code>调用（代码中最后一个）是执行不到的。
其它7个均执行到了，但是它们的返回值都是<code>nil</code>。为什么呢？让我们先阅读一下<a href="https://golang.google.cn/ref/spec#Handling_panics">Go白皮书中列出的规则</a>：

<div class="alert alert-success">
在下面的情况下，<code>recover</code>函数调用的返回值为<code>nil</code>：
<ul>
<li>传递给相应<code>panic</code>函数调用的实参为nil；</li>
<li>当前协程并没有处于恐慌状态；</li>
<li><code>recover</code>函数并未直接在一个延迟函数调用中调用。</li>
</ul>
</div>

<p>
这里我们忽略第一种情况。上例中标号为1/2/3/4的<code>recover</code>调用都属于第二种情况，标号为5和6的<code>recover</code>调用都属于第三种情况。
但是，白皮书中列出的三种情况都没有解释为什么标号为7的<code>recover</code>调用也没有恢复程序中的恐慌。
</p>

我们知道下面的程序中的<code>recover</code>调用将捕获到<code>panic</code>调用抛出的恐慌。
<pre class="line-numbers"><code class="language-go">// example2.go
package main

import (
	"fmt"
)

func main() {
	defer func() {
		fmt.Println( recover() ) // 1
	}()

	panic(1)
}
</code></pre>

<p>
但是，这个简短的程序中的<code>recover</code>调用和前面的例子中的标号为7的<code>recover</code>调用有何本质区别呢？
</p>

<p>
首先，让我们了解一些概念和事实。
</p>
</div>

<h3>概念：函数调用深度、协程执行深度和恐慌深度</h3>

<div>
<p>
每个函数调用都对应着一个相对于当前协程的入口调用的调用深度。
对于主协程中的一个函数调用，它的调用深度是相对于<b><i>main</i></b>入口函数。
</p>

用一个例子说明：
<pre class="line-numbers"><code class="language-go">package main

func main() { // 深度为0
	go func() { // 深度为0
		func() { // 深度为1
		}()

		defer func() { // 深度为1
			defer func() { // 深度为2
			}()
		}()
	}()

	func () { // 深度为1
		func() { // 深度为2
			go func() { // 深度为0
			}()
		}()

		go func() { // 深度为0
		}()
	}()
}
</code></pre>
</div>

<p>
如果一个协程中的当前执行位置处于某个调用中，则我们说此协程的当前执行深度为此调用的深度。
</p>

<p>
一个协程的某个恐慌的深度为此恐慌已经传播到的函数调用深度。
请阅读下一节以对恐慌传播有一个清晰的认识。
</p>

<h3>事实：恐慌只能向更浅的函数深度传播</h3>

<div>
是的，恐慌只会从一个函数传播到此函数的调用函数，而从不会传播到深度更深的被此函数调用的函数中。

<pre class="line-numbers"><code class="language-go">package main

import "fmt"

func main() { // 调用深度为0
	defer func() { // 调用深度为1
		fmt.Println("当前恐慌深度为0（执行深度为1）")
		func() { // 调用深度为2
			fmt.Println("当前恐慌深度为0（执行深度为2）")
			func() { // 调用深度为3
				fmt.Println("当前恐慌深度为0")
			}()
		}()
	}()

	defer fmt.Println("当前恐慌深度为0（执行深度为0）")

	func() { // 调用深度为1
		defer fmt.Println("当前恐慌深度为1（执行深度为1）")
		func() { // 调用深度为2
			defer fmt.Println("当前恐慌深度为2")
			func() { // 调用深度为3
				defer fmt.Println("当前恐慌深度为3")
				panic(1)
			}()
		}()
	}()
}
</code></pre>

<p>
</p>
</div>

<p>
所以，一个恐慌的深度总是单调递减的，它从不增加。另外，一个协程的恐慌深度从不会小于它的执行深度。
</p>

<h3>事实：新生成的恐慌将压制同一深度的老的恐慌</h3>

<div>
一个例子：
<pre class="line-numbers"><code class="language-go">package main

import "fmt"

func main() {
	defer fmt.Println("程序退出时未崩溃")

	defer func() {
		fmt.Println( recover() ) // 3
	}()

	defer fmt.Println("恐慌3将压制恐慌2")
	defer panic(3)
	defer fmt.Println("恐慌2将压制恐慌1")
	defer panic(2)
	panic(1)
}
</code></pre>

输出：
<pre class="output"><code>恐慌2将压制恐慌1
恐慌3将压制恐慌2
3
程序退出时未崩溃
</code></pre>
</div>

<p>
在此例中，恐慌1被恐慌2压制了，恐慌2又被恐慌3压制了。所以，最后被捕获的恐慌值为3。
</p>

<p>
在一个协程中，任何调用深度上最多只能有一个活动的恐慌共存。
特别地，当一个协程的当前执行深度为0时，此协程中只能存在一个活动的恐慌。
</p>

<h3>事实：一个协程中可以有多个活动的恐慌共存</h3>

<div>
一个例子：
<pre class="line-numbers"><code class="language-go">package main

import "fmt"

func main() { // 调用深度为0
	defer fmt.Println("程序崩溃了，因为退出时恐慌3依然未恢复")

	defer func() { // 调用深度为1
		defer func() { // 调用深度为2
			// 恐慌6被消除了。
			fmt.Println( recover() ) // 6
		}()

		// 恐慌3的深度为0，恐慌6的深度为1。
		defer fmt.Println("现在，恐慌3和恐慌6共存")
		defer panic(6) // 将压制恐慌5
		defer panic(5) // 将压制恐慌4
		panic(4) // 不会压制恐慌3，因为恐慌4和恐慌3的深度
		         // 不同。恐慌3为0，而恐慌4的深度为1。
	}()

	defer fmt.Println("现在，只存在恐慌3")
	defer panic(3) // 将压制恐慌2
	defer panic(2) // 将压制恐慌1
	panic(1)
}
</code></pre>

<p>
在这个例子中，两个曾经共存过的恐慌之一（恐慌6）被恢复了。
但是恐慌3在程序退出时仍然没有被恢复，所以此程序在退出时崩溃了。
</p>

输出：
<pre class="output"><code>现在，只存在恐慌3
现在，恐慌3和恐慌6共存
6
程序崩溃了，因为退出时恐慌3依然未恢复
panic: 1
	panic: 2
	panic: 3

goroutine 1 [running]:
...
</code></pre>
</div>


<a class="anchor" id="recover-order"></a>
<h3>事实：更深的恐慌可能被先恢复也可能被后恢复</h3>

<div>

<p>
<b><i>（很抱歉：本节中的代码犯了个低级错误。它证明不了本节标题中的结论。在此段代码中从未有两个恐慌共存的时刻。本节将在一段时间后被删除。）</i></b>
</p>

一个例子：
<pre class="line-numbers"><code class="language-go">package main

import "fmt"

func demo(recoverShallowerPanicAtFirst bool) {
	fmt.Println("====================")
	defer func() {
		if !recoverShallowerPanicAtFirst{
			// 恢复恐慌1
			defer fmt.Println("恐慌", recover(), "被恢复了")
		}
		defer func() {
			//  恢复恐慌2
			fmt.Println("恐慌", recover(), "被恢复了")
		}()
		if recoverShallowerPanicAtFirst {
			//  恢复恐慌1
			defer fmt.Println("恐慌", recover(), "被恢复了")
		}
		defer fmt.Println("现在有两个恐慌共存")
		panic(2)
	}()
	panic(1)
}

func main() {
	demo(true)
	demo(false)
}
</code></pre>

The output:
<pre class="output"><code>====================
现在有两个恐慌共存
恐慌 1 被恢复了
恐慌 2 被恢复了
====================
现在有两个恐慌共存
恐慌 2 被恢复了
恐慌 1 被恢复了
</code></pre>
</div>

<h3>那么，一个<code>recover</code>调用发挥作用的基本规则是什么呢？</h3>

<div>
基本规则很简单：

<div class="alert alert-info">
在一个协程中，假设一个<code>recover</code>调用的调用者函数是<code>f</code>并且此<code>f</code>调用的深度为<code>d</code>，
则此<code>recover</code>调用只有在此<code>f</code>调用是一个延迟调用并且有一个深度为<code>d-1</code>的恐慌存在的情况下才会发挥作用。
此<code>recover</code>调用本身是否是延迟的无关紧要。
</div>

<p>
我认为此规则描述比Go白皮书中的描述更准确。现在，你可以回过头重新看看为什么第一个例子中的标号为7的<code>recover</code>调用没有发挥作用。
</p>
</div>
